SPDX-FileCopyrightText: 2025 Jakub Niemynski & Carla Treinen SPDX-FileCopyrightText: 2025 AlICe laboratory https://alicelab.be
SPDX-License-Identifier: GPL-3.0-or-later
import bpy
import math
import random
import bmeshClear all objects and orphan data
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False)
bpy.ops.outliner.orphans_purge()List (grid) that represents the coordinates in an x by y dimension
def grid(x_size, y_size, empty=False):
grid = []
for i in range(x_size):
row = []
for j in range(y_size):
if not empty:
x = i
y = j
row.append((x, y))
else:
row.append(None)
grid.append(row)
return gridStepcount which whill be used later on
def shift(pattern, step=1):
part_one = pattern[step:]
part_two = pattern[:step]
new_pattern = part_one + part_two
return new_patternCheck if two points are direct neighbors or diagonal neighbors
def is_direct_neighbour(p1, p2):
x1, y1 = p1
x2, y2 = p2
return (x1 == x2 and abs(y1 - y2) == 1) or ( # Horizontal neighbor
y1 == y2 and abs(x1 - x2) == 1
) # Vertical neighbordef is_diagonal_neighbour(p1, p2):
x1, y1 = p1
x2, y2 = p2
return abs(x1 - x2) == 1 and abs(y1 - y2) == 1 # Diagonal neighborFunction to create a cylinder at a given location
def create_cylinder(location, radius, height, name):
bpy.ops.mesh.primitive_cylinder_add(radius=radius, depth=height, location=location)
cylinder = bpy.context.object
cylinder.name = nameFunction to create a mesh from vertices and faces
def create_mesh(name, vertices, faces):
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(vertices, [], faces)
mesh.update()
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj)Function to create grid
def create_grid(axis, base_location, dimensions, num_copies, spacing):Creates a grid of cubes along a specified axis.
:param axis: The axis along which to create the grid (‘x’ or ‘y’). :param base_location: Tuple (x, y, z) for the starting location of the grid. :param dimensions: Tuple (width, depth, height) for the cube’s dimensions. :param num_copies: Number of cubes to create in the grid. :param spacing: Space between cubes.
width, depth, height = dimensionsCreate the initial cube
bpy.ops.mesh.primitive_cube_add(size=1, location=base_location)
cube = bpy.context.object
cube.scale = (width, depth, height)
cube.name = f"CustomCube_{axis}_Grid"Ensure the mesh is unique
cube.data = cube.data.copy()Duplicate cubes along the specified axis
for i in range(1, num_copies):
new_cube = cube.copy()
new_cube.data = new_cube.data.copy() # Ensure no shared mesh data
if axis == "x":
new_cube.location.x = base_location[0] + i * (width + spacing)
elif axis == "y":
new_cube.location.y = base_location[1] + i * (depth + spacing)
bpy.context.collection.objects.link(new_cube)Define parameters for the grids
grid_params = [
("x", (0, 6, -3), (0.1, 12, 0.1), 11, 0.9), # Grid along X-axis (lower level)
("y", (6, 0, -3), (12, 0.1, 0.1), 11, 0.9), # Grid along Y-axis (lower level)
("x", (0, 6, 3), (0.1, 12, 0.1), 11, 0.9), # Grid along X-axis (upper level)
("y", (6, 0, 3), (12, 0.1, 0.1), 11, 0.9), # Grid along Y-axis (upper level)
]Create the grids
for params in grid_params:
create_grid(*params)
print("Grids created successfully!")Initial grid dimensions
size = 12Generate coordinates for grids
grid_down = grid(size, size)
grid_up = grid(size, size)
empty_down = grid(size, size, empty=False)
empty_up = grid(size, size, empty=False)Define voice patterns
voice_down = []
while True not in voice_down:
voice_down = [random.choice([True, False]) for _ in range(size)]
voice_up = voice_downFill grid_down without shifts
for row in grid_down:
for coord, is_voice in zip(row, voice_down):
if not is_voice:
r = grid_down.index(row)
i = row.index(coord)
grid_down[r][i] = None
empty_down[r][i] = coord
print("Initial Grid Down:\t", grid_down)Fill grid_up with consistent shifts
shift_amount = random.randint(1, size)
current_voice_up = shift(voice_up, shift_amount)
for row in grid_up:
for coord, is_voice in zip(row, current_voice_up):
if not is_voice:
r = grid_up.index(row)
i = row.index(coord)
grid_up[r][i] = None
empty_up[r][i] = coord
current_voice_up = shift(current_voice_up, shift_amount)
print("Initial Grid UP:\t", grid_up)Create double_columns and clean grid_up/grid_down
double_columns = []
new_grid_up = []
new_grid_down = []
for row_up, row_down in zip(grid_up, grid_down):
for up_coord, down_coord in zip(row_up, row_down):
if up_coord and down_coord:
double_columns.append(up_coord)
r = grid_up.index(row_up)
i = row_up.index(up_coord)
grid_up[r][i] = None
grid_down[r][i] = None
print("Double Columns:\t\t", double_columns)
print("New Grid Down:\t\t", grid_down)
print("New Grid UP:\t\t", grid_up)Populate new_grid_up and new_grid_down
for row in grid_up:
for coord in row:
if coord:
new_grid_up.append(coord)
for row in grid_down:
for coord in row:
if coord:
new_grid_down.append(coord)Parameters for cylinders
height = 3.5
radius_GridDown = 0.36
radius_GridUp = 0.18
z_GridDown = 1.25
z_GridUp = -1.25Create cylinders for double_columns
for coord in double_columns:
x, y = coord
create_cylinder(
location=(x, y, z_GridDown),
radius=radius_GridDown,
height=height,
name=f"Cylinder_GridDown_{coord}",
)
create_cylinder(
location=(x, y, z_GridUp),
radius=radius_GridUp,
height=height,
name=f"Cylinder_GridUp_{coord}",
)Identify neighboring pairs (direct and diagonal neighbors)
neighbour_pairs = []
ortho = []
for coord1 in new_grid_up + new_grid_down:
for coord2 in new_grid_up + new_grid_down:
if is_direct_neighbour(coord1, coord2):
neighbour_pairs.append((coord1, coord2))
ortho.append(coord1)
ortho.append(coord2)
elif (coord1 not in ortho and coord2 not in ortho) and is_diagonal_neighbour(
coord1, coord2
):
neighbour_pairs.append((coord1, coord2))
print("Neighbor Pairs (Direct and Diagonal):")
for pair in neighbour_pairs:
print(pair)Create meshes for neighboring pairs (direct and diagonal neighbors)
for coord1, coord2 in neighbour_pairs:
x1, y1 = coord1
x2, y2 = coord2Check grid membership and determine z-values
if coord1 in new_grid_down:
z1_low, z1_high = -3, 0
elif coord1 in new_grid_up:
z1_low, z1_high = 0, 3
else:
continue
if coord2 in new_grid_down:
z2_low, z2_high = -3, 0
elif coord2 in new_grid_up:
z2_low, z2_high = 0, 3
else:
continueDefine vertices
vertices = [
(x1, y1, z1_low),
(x1, y1, z1_high),
(x2, y2, z2_low),
(x2, y2, z2_high),
]Define faces
faces = [(0, 1, 3, 2)]Create mesh
mesh_name = f"Mesh_{coord1}_{coord2}"
create_mesh(mesh_name, vertices, faces)
print("Cylinders and meshes for direct and diagonal neighbors created successfully!")Solidify meshes
for obj in bpy.data.objects:
if "Mesh_" in obj.name:add solidify
mod2 = bpy.data.objects[obj.name].modifiers.new(
name="solidify", type="SOLIDIFY"
)
mod2.thickness = 0.15Random Cube Placement
random_range = 12 # Range for random placement of x and y
random_x = random.uniform(3, random_range - 3)
random_y = random.uniform(3, random_range - 3)
z_location = 0 # Fixed Z location for the cubeCube dimensions
cube_size_x = 6
cube_size_y = 6
cube_size_z = 7Create the cube = subtractive object
bpy.ops.mesh.primitive_cube_add(size=1, location=(random_x, random_y, z_location))
cube = bpy.context.object
cube.scale = (cube_size_x, cube_size_y, cube_size_z)
cube.name = "BooleanCube"Apply Boolean Modifier to All Mesh Objects
for obj in bpy.context.scene.objects:
if obj == cube or obj.type != "MESH":
continueAdd the boolean modifier
boolean_modifier = obj.modifiers.new(name="BooleanModifier", type="BOOLEAN")
boolean_modifier.object = cube
boolean_modifier.operation = "INTERSECT"Apply the modifier
bpy.context.view_layer.objects.active = obj
try:
bpy.ops.object.modifier_apply(modifier=boolean_modifier.name)
print(f"Boolean applied to {obj.name}.")
except Exception as e:
print(f"Failed to apply boolean on {obj.name}: {e}")Remove the cube after all operations are complete
bpy.data.objects.remove(cube, do_unlink=True)
print("Cube removed.")
print("Final cube successfully created.")